OFFSET label is the label's address within its segment, as a number. SEG label is the segment the label lives in. Both are computed when the program is assembled and linked, not when it runs, so MOV DX, OFFSET MSG1 is a move of an immediate constant. With .MODEL, MASM measures offsets from the start of the group (DGROUP), which is what DS points at.

Prints Hi! and then 6, the size of MSG1 in bytes.
MOV DX, OFFSET MSG1 ; DX = the address of MSG1
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AX, OFFSET MSG2 - OFFSET MSG1 ; distance between two labels
ADD AL, '0' ; digit -> character
MOV DL, AL
MOV AH, 02h ; DOS: print DL
INT 21h
MSG1 DB 'Hi!', 13, 10, '$'
MSG2 DB 0

The classic mistake

Without OFFSET, MASM treats a data label as the memory it names. MOV DX, MSG1 loads the first two bytes of the string ("Hi") into DX, not its address, and the following print shows garbage. Whenever you want a pointer, write OFFSET (or use LEA).

OFFSET or LEA

MOV reg, OFFSET label is one byte shorter than LEA reg, label and gives the same value. LEA is needed when registers take part in the address, which OFFSET cannot express since it is a constant.

SEG

A segment value is not known until DOS loads the program, so SEG leaves a relocation that the loader patches in a .EXE. A .COM has no relocation table: there SEG cannot be used, and you take the segment from CS at run time instead. In a .EXE the usual start looks like this:

MOV AX, SEG MSG1 ; .EXE only: patched by the loader
MOV DS, AX ; (the same as MOV AX, @data with .MODEL)
MOV DX, OFFSET MSG1

Pitfalls

OFFSET gives an offset, not a full address: it means something only together with the right segment register. DW label stores the offset like DW OFFSET label; DD label stores offset and segment and so needs a relocation.