The one thing to understand first
Most people think only VACUUM removes dead rows. It does not. A plain SELECT can quietly clean up dead row versions on the pages it touches — a mechanism called <a class="sev1-termlink" href="https://thesev1database.com/glossary/heap-pruning/" title="Heap pruning">heap pruning. Whether a row is allowed to be removed comes down to a single moving boundary: the xmin horizon, the oldest transaction snapshot still alive in the system. Understand that boundary and you understand why a forgotten open transaction makes an entire database bloat.
What pruning actually does (and does not do)
The logic lives in src/backend/access/heap/pruneheap.c. When a scan pins a heap page, heap_page_prune_opt() decides whether it is worth cleaning. Pruning is intra-page only: it walks the tuples on that one 8 KB page and reclaims the space of dead versions, but it does not touch indexes — that remains full VACUUM‘s job.
Every tuple on a page is reached through a line pointer (ItemIdData), a tiny slot in the page header. Pruning rewrites those line pointers into one of four states: