Target first and last items and append if greater than 2
The way I would think of it is: I print the category title, then if this is not the last category I print the separator (and or ,). I would use the loop variable revindex to distinguish the next-to-last category (which gets the and) from the others (which get the ,). loop.revindex is like loop.index except that it counts the number of iterations from the end. So your code starts to look like this:

{% for category in categories %}
  {{category.title}}
  {% if not loop.last %}
    {{ loop.revindex == 2 ? ' and' : ',' }}
  {% endif %}
{% endfor %}


That code will leave a space before the ,, which you can fix by sticking everything on one line:

{% for category in categories %}
  {{category.title}}{% if not loop.last %}{{ loop.revindex == 2 ? ' and' : ',' }}{% endif %}
{% endfor %}