Now I think I've figured out the problem. There are at least two problem on my code:
Problem 1) Lines are under other polygon fills
The lines and fills of polygons are drawn geometry by geometry, so it's natural that we can't see the underlying lines.
I should have drawn them independently:
library(ggplot2)
library(patchwork)
nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
p1 <- ggplot(nc) +
geom_sf(linetype = "dotted", size = 1.5) +
theme_minimal()
p2 <- ggplot(nc) +
# draw polygon fills
geom_sf(colour = "transparent") +
# draw polygon lines
geom_sf(fill = "transparent", linetype = "dotted", size = 1.5) +
theme_minimal()
p1 / p2

Problem 2) Lines are overwrapped
The boundaries between polygons are drawn multiple times. I should have extract the boundaries and merge them beforehand (Are there easier way to do this...?):
library(sf)
nc_12 <- tibble::rownames_to_column(nc[1:2,], "id")
nc_12_merged <- nc_12 %>%
st_boundary() %>%
st_union() %>%
st_line_merge() %>%
st_sf()
p1 <- ggplot(nc_12) +
geom_sf(aes(colour = id), fill = "transparent", linetype = "dotted", size = 4) +
theme_minimal()
p2 <- ggplot(nc_12_merged) +
geom_sf(colour = "black", fill = "transparent", linetype = "dotted", size = 4) +
theme_minimal()
p1 / p2
