Hi
1 Make sure that you have enabled the IRC48M by setting the HIRCEN bit in MCG_MC.
2. TPMx_MOD should be (2400 - 1) to give the exact frequency you want.
3. PTMx_CnSC of 0x28 is a known good value for PWM - yours (0x14) may be Ok but I didn't check.
4. TMPx_SC is best written as final command rather than before setting up the details (since it enables the operation)
5. I suspect the major issue is the TPMx_SC value since 4 divides the clock by 16 but doesn't enable the operation. Probably you wanted to set 8 in order to have a divide by 1 and start it.
I would always work with defines for the control bits to avoid such potential errors.
Below is the uTasker code to do this on the KL03, plus the simulated KL03 registers so that you can check your register values for comparison:
PWM_INTERRUPT_SETUP pwm_setup;
pwm_setup.int_type = PWM_INTERRUPT;
pwm_setup.pwm_mode = (PWM_IRC48M_CLK | PWM_PRESCALER_1); // clock PWM timer from the IRC48M clock with /1 pre-scaler
pwm_setup.int_handler = 0; // no user interrupt call-back or DMA on PWM cycle
pwm_setup.pwm_frequency = PWM_FREQUENCY(20000, 1); // generate 20kHz on PWM output
pwm_setup.pwm_value = _PWM_PERCENT(50, pwm_setup.pwm_frequency); // 50% PWM (high/low)
pwm_setup.pwm_reference = (_TIMER_0 | 0); // timer module 0, channel 0
fnConfigureInterrupt((void *)&pwm_setup); // enter configuration
pwm_setup.pwm_reference = (_TIMER_0 | 1); // timer module 0, channel 1
fnConfigureInterrupt((void *)&pwm_setup); // enter configuration
pwm_setup.pwm_reference = (_TIMER_1 | 0); // timer module 1, channel 0
fnConfigureInterrupt((void *)&pwm_setup); // enter configuration
TPM0:

TPM1:

Regards
Mark
P.S. As reference, I have attached the uTasker PWM interface code which is compatible for TPM or FlexTimer PWM operation on all chips, including interrupt and DMA operations.