To just get the count as outlined:
SELECT count(*) AS msg_ct
FROM  (
   SELECT DISTINCT ON (conversation_id) *
   FROM   messages
   ORDER  BY conversation_id, created_at DESC
   ) sub
WHERE NOT read;
You do no need to include the parent table conversation here at all.
The subquery returns the last row per conversation ("ordered by messages.created_at").
Details here:  Select first row in each GROUP BY group?
The outer query counts those where read is FALSE.
Alternative with NOT EXISTS:
SELECT count(*) AS msg_ct
FROM   messages m
WHERE  NOT read
AND NOT EXISTS (
   SELECT 1
   FROM   messages m1
   WHERE  m1.conversation_id = m.conversation_id
   AND    m1.created_at > m.created_at
   );
Might be faster for big tables. You'd have to test with EXPLAIN ANALYZE.